//-----------------------------------------------------------------------
// <copyright file="AndroidNativeHeadsetProvider.cs" company="Google Inc.">
// Copyright 2017 Google Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// </copyright>
//-----------------------------------------------------------------------

// This provider is only available on an Android device.
#if UNITY_ANDROID && !UNITY_EDITOR
using Gvr;
using System;
using System.Runtime.InteropServices;
using UnityEngine;
#if UNITY_2017_2_OR_NEWER
using UnityEngine.XR;
#else
using XRDevice = UnityEngine.VR.VRDevice;
#endif  // UNITY_2017_2_OR_NEWER

/// @cond
namespace Gvr.Internal
{
    class AndroidNativeHeadsetProvider : IHeadsetProvider
    {
        private IntPtr gvrContextPtr = XRDevice.GetNativePtr();
        private GvrValue gvrValue = new GvrValue();
        private gvr_event_header gvrEventHeader = new gvr_event_header();
        private gvr_recenter_event_data gvrRecenterEventData = new gvr_recenter_event_data();

        // |gvr_event| C struct is spec'd to be up to 512 bytes in size.
        private byte[] gvrEventBuffer = new byte[512];
        private GCHandle gvrEventHandle;
        private IntPtr gvrEventPtr;
        private IntPtr gvrPropertiesPtr;
        private int supportsPositionalTracking = -1;

        /// Used only as a temporary placeholder to avoid allocations.
        /// Do not use this for storing state.
        private MutablePose3D transientRecenteredPose3d = new MutablePose3D();

        public bool SupportsPositionalTracking
        {
            get
            {
                if (supportsPositionalTracking < 0)
                {
                    supportsPositionalTracking =
                        gvr_is_feature_supported(gvrContextPtr, (int)gvr_feature.HeadPose6dof) ? 1 : 0;
                }

                return supportsPositionalTracking > 0;
            }
        }

        internal AndroidNativeHeadsetProvider()
        {
            gvrEventHandle = GCHandle.Alloc(gvrEventBuffer, GCHandleType.Pinned);
            gvrEventPtr = gvrEventHandle.AddrOfPinnedObject();
        }

        ~AndroidNativeHeadsetProvider()
        {
            gvrEventHandle.Free();
        }

        public void PollEventState(ref HeadsetState state)
        {
            GvrErrorType eventType;
            try
            {
                eventType = (GvrErrorType)gvr_poll_event(gvrContextPtr, gvrEventPtr);
            }
            catch (EntryPointNotFoundException)
            {
                Debug.LogError("GvrHeadset not supported by this version of Unity. " +
                "Support starts in 5.6.3p3 and 2017.1.1p1.");
                throw;
            }

            if (eventType == GvrErrorType.NoEventAvailable)
            {
                state.eventType = GvrEventType.Invalid;
                return;
            }

            Marshal.PtrToStructure(gvrEventPtr, gvrEventHeader);
            state.eventFlags = gvrEventHeader.flags;
            state.eventTimestampNs = gvrEventHeader.timestamp;
            state.eventType = (GvrEventType)gvrEventHeader.type;

            // Event data begins after header.
            IntPtr eventDataPtr = new IntPtr(gvrEventPtr.ToInt64() + Marshal.SizeOf(gvrEventHeader));

            if (state.eventType == GvrEventType.Recenter)
            {
                Marshal.PtrToStructure(eventDataPtr, gvrRecenterEventData);
                _HandleRecenterEvent(ref state, gvrRecenterEventData);
                return;
            }
        }

        public bool TryGetFloorHeight(ref float floorHeight)
        {
            if (!_GvrGetProperty(gvr_property_type.TrackingFloorHeight, gvrValue))
            {
                return false;
            }

            floorHeight = gvrValue.ToFloat();
            return true;
        }

        public bool TryGetRecenterTransform(ref Vector3 position, ref Quaternion rotation)
        {
            if (!_GvrGetProperty(gvr_property_type.RecenterTransform, gvrValue))
            {
                return false;
            }

            transientRecenteredPose3d.Set(gvrValue.ToMatrix4x4());
            position = transientRecenteredPose3d.Position;
            rotation = transientRecenteredPose3d.Orientation;
            return true;
        }

        public bool TryGetSafetyRegionType(ref GvrSafetyRegionType safetyType)
        {
            if (!_GvrGetProperty(gvr_property_type.SafetyRegion, gvrValue))
            {
                return false;
            }

            safetyType = (GvrSafetyRegionType)gvrValue.ToInt32();
            return true;
        }

        public bool TryGetSafetyCylinderInnerRadius(ref float innerRadius)
        {
            if (!_GvrGetProperty(gvr_property_type.SafetyCylinderInnerRadius, gvrValue))
            {
                return false;
            }

            innerRadius = gvrValue.ToFloat();
            return true;
        }

        public bool TryGetSafetyCylinderOuterRadius(ref float outerRadius)
        {
            if (!_GvrGetProperty(gvr_property_type.SafetyCylinderOuterRadius, gvrValue))
            {
                return false;
            }

            outerRadius = gvrValue.ToFloat();
            return true;
        }

#region PRIVATE_HELPERS
        /// Returns true if a property was available and retrieved.
        private bool _GvrGetProperty(gvr_property_type propertyType, GvrValue valueOut)
        {
            gvr_value_type valueType = GetPropertyValueType(propertyType);
            if (valueType == gvr_value_type.None)
            {
                Debug.LogError("Unknown gvr property " + propertyType + ".  Unable to type check.");
            }

            if (gvrPropertiesPtr == IntPtr.Zero)
            {
                try
                {
                    gvrPropertiesPtr = gvr_get_current_properties(gvrContextPtr);
                }
                catch (EntryPointNotFoundException)
                {
                    Debug.LogError("GvrHeadset not supported by this version of Unity. " +
                    "Support starts in 5.6.3p3 and 2017.1.1p1.");
                    throw;
                }
            }

            if (gvrPropertiesPtr == IntPtr.Zero)
            {
                return false;
            }

            // Assumes that gvr_properties_get (C API) will only ever return
            // GvrErrorType.None or GvrErrorType.NoEventAvailable.
            bool success =
                (GvrErrorType.None ==
                (GvrErrorType)gvr_properties_get(gvrPropertiesPtr, propertyType, valueOut.BufferPtr));
            if (success)
            {
                valueOut.Parse();
                success = (valueType == gvr_value_type.None || valueOut.TypeEnum == valueType);
                if (!success)
                {
                    Debug.LogError("GvrGetProperty " + propertyType + " type mismatch, expected "
                    + valueType + " got " + valueOut.TypeEnum);
                }
            }

            return success;
        }

        private void _HandleRecenterEvent(ref HeadsetState state, gvr_recenter_event_data eventData)
        {
            state.recenterEventType = (GvrRecenterEventType)eventData.recenter_event_type;
            state.recenterEventFlags = eventData.recenter_event_flags;
            Matrix4x4 gvrMatrix = GvrMathHelpers.ConvertFloatArrayToMatrix(eventData.pose_transform);
            GvrMathHelpers.GvrMatrixToUnitySpace(
                gvrMatrix, out state.recenteredPosition, out state.recenteredRotation);
        }
#endregion  // PRIVATE_HELPERS

#region GVR_TYPE_HELPERS
        private gvr_value_type GetPropertyValueType(gvr_property_type propertyType)
        {
            gvr_value_type propType = gvr_value_type.None;
            switch (propertyType)
            {
                case gvr_property_type.TrackingFloorHeight:
                    propType = gvr_value_type.Float;
                    break;
                case gvr_property_type.RecenterTransform:
                    propType = gvr_value_type.Mat4f;
                    break;
                case gvr_property_type.SafetyRegion:
                    propType = gvr_value_type.Int;
                    break;
                case gvr_property_type.SafetyCylinderInnerRadius:
                    propType = gvr_value_type.Float;
                    break;
                case gvr_property_type.SafetyCylinderOuterRadius:
                    propType = gvr_value_type.Float;
                    break;
            }

            return propType;
        }

        /// Helper class to parse |gvr_value| structs into the varied data types it could contain.
        /// NOTE: Does NO type checking on value conversions.  |_GvrGetProperty| checks types.
        private class GvrValue
        {
            private static readonly int HEADER_SIZE = Marshal.SizeOf(typeof(gvr_value_header));
            private gvr_value_header valueHeader = new gvr_value_header();

            // |gvr_value| C struct is spec'd to be up to 256 bytes in size.
            private byte[] buffer = new byte[256];
            private IntPtr bufferPtr;
            private IntPtr valuePtr;
            private GCHandle bufferHandle;

            public GvrValue()
            {
                bufferHandle = GCHandle.Alloc(buffer, GCHandleType.Pinned);
                bufferPtr = bufferHandle.AddrOfPinnedObject();

                // Value portion starts after the header.
                valuePtr = new IntPtr(bufferPtr.ToInt64() + HEADER_SIZE);
            }

            ~GvrValue()
            {
                bufferHandle.Free();
            }

            /// Gets the ptr to a buffer that can be used as an argument to |gvr_properties_get|.
            public IntPtr BufferPtr
            {
                get
                {
                    return bufferPtr;
                }
            }

            public gvr_value_type TypeEnum
            {
                get
                {
                    return valueHeader.value_type;
                }
            }

            /// Parse the header of the current buffer.  This should be called after the contents of
            /// the buffer have been altered e.g. by a call to |gvr_properties_get|.
            public void Parse()
            {
                Marshal.PtrToStructure(bufferPtr, valueHeader);
            }

            public int ToInt32()
            {
                return BitConverter.ToInt32(buffer, HEADER_SIZE);
            }

            public long ToInt64()
            {
                return BitConverter.ToInt64(buffer, HEADER_SIZE);
            }

            public float ToFloat()
            {
                return BitConverter.ToSingle(buffer, HEADER_SIZE);
            }

            public double ToDouble()
            {
                return BitConverter.ToDouble(buffer, HEADER_SIZE);
            }

            public Vector2 ToVector2()
            {
                return (Vector2)Marshal.PtrToStructure(valuePtr, typeof(Vector2));
            }

            public Vector3 ToVector3()
            {
                return (Vector3)Marshal.PtrToStructure(valuePtr, typeof(Vector3));
            }

            public Vector4 ToVector4()
            {
                return (Vector4)Marshal.PtrToStructure(valuePtr, typeof(Vector4));
            }

            public Quaternion ToQuaternion()
            {
                return (Quaternion)Marshal.PtrToStructure(valuePtr, typeof(Quaternion));
            }

            public gvr_rectf ToGvrRectf()
            {
                return (gvr_rectf)Marshal.PtrToStructure(valuePtr, typeof(gvr_rectf));
            }

            public gvr_recti ToGvrRecti()
            {
                return (gvr_recti)Marshal.PtrToStructure(valuePtr, typeof(gvr_recti));
            }

            public Matrix4x4 ToMatrix4x4()
            {
                Matrix4x4 mat4 = (Matrix4x4)Marshal.PtrToStructure(valuePtr, typeof(Matrix4x4));

                // Transpose the matrix from row-major (GVR) to column-major (Unity),
                // and change from LHS to RHS coordinates.
                return Pose3D.FlipHandedness(mat4.transpose);
            }
        }
#endregion  // GVR_TYPE_HELPERS

#region GVR_NATIVE_STRUCTS
        [StructLayout(LayoutKind.Sequential)]
        private class gvr_recenter_event_data
        {
            // gvr_recenter_event_type
            internal int recenter_event_type;

            // gvr_flags
            internal uint recenter_event_flags;

            // gvr_mat4f = float[4][4]
            [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)]
            internal float[] pose_transform = new float[16];
        }

        [StructLayout(LayoutKind.Explicit)]
        private class gvr_event_header
        {
            [FieldOffset(0)]
            internal long timestamp;

            // gvr_event_type
            [FieldOffset(8)]
            internal int type;

            // gvr_flags
            [FieldOffset(12)]
            internal int flags;

            // Event specific data starts at offset 16.
        }

        [StructLayout(LayoutKind.Explicit)]
        private class gvr_value_header
        {
            [FieldOffset(0)]
            internal gvr_value_type value_type;

            // gvr_flags
            [FieldOffset(4)]
            internal int flags;

            // Value data starts at offset 8.
        }

        [StructLayout(LayoutKind.Sequential)]
        public struct gvr_sizei
        {
            internal int width;
            internal int height;
        }

        [StructLayout(LayoutKind.Sequential)]
        public struct gvr_recti
        {
            internal int left;
            internal int right;
            internal int bottom;
            internal int top;
        }

        [StructLayout(LayoutKind.Sequential)]
        public struct gvr_rectf
        {
            internal float left;
            internal float right;
            internal float bottom;
            internal float top;
        }

#endregion  // GVR_NATIVE_STRUCTS

#region GVR_C_API
        private const string DLL_NAME = GvrActivityHelper.GVR_DLL_NAME;

        [DllImport(DLL_NAME)]
        internal static extern bool gvr_is_feature_supported(IntPtr gvr_context, int feature);

        [DllImport(DLL_NAME)]
        private static extern int gvr_poll_event(IntPtr gvr_context, IntPtr event_out);

        [DllImport(DLL_NAME)]
        private static extern IntPtr gvr_get_current_properties(IntPtr gvr_context);

        [DllImport(DLL_NAME)]
        private static extern int gvr_properties_get(
            IntPtr gvr_properties, gvr_property_type property_type, IntPtr value_out);
#endregion  // GVR_C_API
    }
}

/// @endcond
#endif  // UNITY_ANDROID && !UNITY_EDITOR
